package com.okbihuo.perm.component.file.logic;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.OSSObject;
import com.okbihuo.perm.component.file.config.FileConfig;
import com.okbihuo.perm.component.file.domain.ComFile;
import com.okbihuo.perm.component.file.domain.ComFileContent;
import com.okbihuo.perm.component.file.domain.ComFileReference;
import com.okbihuo.perm.component.file.domain.type.FileStoreType;
import com.okbihuo.perm.component.file.domain.type.FileType;
import com.okbihuo.perm.component.file.service.ComFileContentService;
import com.okbihuo.perm.component.file.service.ComFileReferenceService;
import com.okbihuo.perm.component.file.service.ComFileService;
import com.okbihuo.perm.core.tool.api.BaseException;
import com.okbihuo.perm.core.tool.api.ResultCode;
import com.okbihuo.perm.core.tool.jackson.JsonUtil;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Objects;


/**
 * 文件逻辑
 * @author Xyang
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class FileLogic {

    private final FileConfig fileConfig;

    private final ComFileService fileService;

    private final ComFileContentService fileContentService;

    private final ComFileReferenceService fileReferenceService;

    /**
     * 获取阿里云oss 操作实例
     */
    private OSS getAliyunOss() {
        // 创建OSSClient实例。
        return new OSSClientBuilder().build(fileConfig.aliyunEndPoint, fileConfig.aliyunAccessKeyId,  fileConfig.aliyunAccessKeySecret);
    }

    /**
     * 文件路径分隔符
     */
    private static String fileSeparator = "/";
    /**
     * 上传文件
     * @param file 文件
     * @param dir 存入阿里云或本地时都有dir控制上传位置, 必须以斜杠开头/
     * @return 文件结果
     */
    public ComFile upload(MultipartFile file, String dir) throws BaseException {
        try {
            String filename = file.getOriginalFilename();
            if(dir==null || Objects.equals(dir, "")) {
                dir = "";
            } else {
                if(dir.startsWith(fileSeparator)) {
                    throw new BaseException(ResultCode.DATA_NOT_FOUND,"DIR不能斜杠开头");
                }
            }
            log.debug("收到文件{}上传请求", filename);
            InputStream inputStream = file.getInputStream();
            byte[] pictureData = new byte[(int) file.getSize()];
            int r = inputStream.read(pictureData);
            log.debug("收到文件{}数据，大小{}", filename, r);
            return createFile(filename, dir, pictureData);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,e.getMessage());
        }
    }

    /**
     * 创建文件记录
     * @param filename 文件名
     * @param dir 文件夹路径
     * @param pictureData 文件内容
     * @return 文件对象
     * @throws BaseException 上传异常
     */
    @Transactional(rollbackFor = Exception.class)
    public ComFile createFile(String filename, String dir, byte[] pictureData) throws BaseException {
        // 创建文件内容,但不保存
        ComFileContent fileContent = fileContentService.createNewFileContent(filename, pictureData);
        String md5 = fileContent.getMd5();
        log.debug("收到文件{}数据，计算MD5{}", filename, fileContent.getMd5());
        // 从数据里搜索是否有相同的MD5，如果有, 则上传文件记录指向原有文件内容
        ComFile exist = fileService.getOneByMd5(md5);
        ComFile newFile = ComFile.create(filename,dir, fileConfig.fileStoreType, md5, pictureData.length);
        newFile.setExpiresAt(LocalDateTime.now().plusDays(fileConfig.expiresDay));
        log.debug("新建文件记录{}", JsonUtil.toJson(newFile));
        if(exist ==null) {
            // 判断存储类型，对内容做不同的处理
            FileStoreType storeType = FileStoreType.valueOf(fileConfig.fileStoreType);
            newFile.setStoreType(storeType.name());
            switch (storeType) {
                case aliyun:
                    uploadToAliyun(dir, newFile, fileContent);
                    break;
                case local:
                    saveToLocal(dir, newFile, fileContent);
                    break;
                case table:
                    saveToTable(newFile, fileContent);
                    break;
                default:
                    throw new BaseException(ResultCode.DATA_NOT_FOUND, "文件存储方式配置有误");
            }
        }else {
            // 指向原有内容
            fileService.pointToExist(newFile, exist);
        }
        return fileService.saveOrUpdateAndReturn(newFile);
    }


    /**
     * 创建文件记录
     * @param filename 文件名
     * @param dir 文件夹路径
     * @param bufferedImage 图片内容
     * @return 文件对象
     * @throws BaseException 上传异常
     */
    public ComFile createFile(String filename, String dir, BufferedImage bufferedImage) throws BaseException {
        ByteArrayOutputStream outStream =new ByteArrayOutputStream();
        try {
            ImageIO.write(bufferedImage, "jpg", outStream);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return createFile(filename,dir,outStream.toByteArray());
    }
    /**
     * 保存内容到数据库
     */
    private void saveToTable(ComFile file, ComFileContent fileContent) throws BaseException {
        try {
            log.debug("尝试将文件内容存入数据库{}", fileContent.getFilename());
            fileContent = fileContentService.saveOrUpdateAndReturn(fileContent);
            // 建立对应关系
            file.setLocation(fileContent.getId().toString());
        } catch (BaseException e) {
            log.error("文件内容{}存入数据库失败", fileContent.getFilename());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"图片上传失败");
        }
    }

    /**
     * 保存内容到阿里云
     */
    private void uploadToAliyun(String dir, ComFile file, ComFileContent fileContent) throws BaseException {

// 创建OSSClient实例。
        OSS ossClient = getAliyunOss();

// 上传Byte数组。
        try {
            String objectName = dir + fileSeparator + file.getName();
            file.setLocation(objectName);
            log.info("开始上传文件至阿里云");
            ossClient.putObject(fileConfig.aliyunBucket, objectName, new ByteArrayInputStream(fileContent.getContent()));
        }catch (OSSException oe) {
            log.error("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            log.error("Error Message: " + oe.getErrorMessage());
            log.error("Error Code:       " + oe.getErrorCode());
            log.error("Request ID:      " + oe.getRequestId());
            log.error("Host ID:           " + oe.getHostId());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"图片上传至阿里云失败");
        } catch (ClientException ce) {
            log.error("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            log.error("Error Message: " + ce.getMessage());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"图片上传至阿里云失败");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

    }

    /**
     * TODO 保存在本地
     */
    private String saveToLocal(String dir, ComFile file, ComFileContent fileContent){
        return "";
    }

    /**
     * 直接下载文件
     */
    public void download(Integer fileId, boolean forceDownload, HttpServletResponse response) throws BaseException {
        ComFile find = fileService.getById(fileId);
        if(find==null) {
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"没有对应文件");
        }
        // 获取内容
        byte[] data = getFileBytes(find);
        String filename = find.getOriginalName();
        // 转一次格式
        filename = new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);;
        OutputStream outputStream = null;
        // 前端缓存
        response.addHeader("Cache-Control", "public, max-age=86400, stale-while-revalidate=604800");
        response.addHeader("Content-Length", "" + data.length);
        response.setCharacterEncoding("UTF-8");
        if(FileType.image.name().equals(find.getType())){
            response.setContentType("image/jpeg");
            if(forceDownload){
                response.addHeader("Content-Disposition", "attachment;filename=" + filename);
            }
        }else{
            response.addHeader("Content-Disposition", "attachment;filename=" + filename);
            response.setContentType("application/force-download");
        }
        try {
            outputStream = response.getOutputStream();
            outputStream.write(data);
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取文件二进制流
     */
    public byte[] getFileBytes(ComFile file) throws BaseException {
        if(StringUtils.isBlank(file.getLocation())) {
            log.error("对应文件{}访问地址数据丢失", file.getId());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"对应文件访问地址数据丢失");
        }
        // 判断存储类型，对内容做不同的处理
        FileStoreType storeType = FileStoreType.valueOf(file.getStoreType());
        switch (storeType) {
            case aliyun:
                return getFileBytesFromAliyun(file);
            case table:
                return getFileBytesFromTable(file);
            case local:
            default:
                throw new BaseException(ResultCode.DATA_NOT_FOUND, "文件存储方式配置有误");
        }
    }


    public byte[] getFileBytes(Long fileId) throws BaseException{
        ComFile file = fileService.getById(fileId);
        return getFileBytes(file);
    }


    /**
     * 从表里面获取文件内容
     */
    private byte[] getFileBytesFromTable(ComFile file) {
        Integer contentId = Integer.valueOf(file.getLocation());
        return fileContentService.getById(contentId).getContent();
    }
    /**
     * 获取文件二进制流
     */
    private byte[] getFileBytesFromAliyun(ComFile file) throws BaseException {
        // 创建OSSClient实例。
        OSS ossClient = getAliyunOss();
        try {
            // ossObject包含文件所在的存储空间名称、文件名称、文件元信息以及一个输入流。
            OSSObject ossObject = ossClient.getObject(fileConfig.aliyunBucket, file.getLocation());
            @Cleanup InputStream objectContent = ossObject.getObjectContent();
            @Cleanup ByteArrayOutputStream result = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int len;
            while ((len = objectContent.read(b)) != -1 ) {
                result.write(b, 0, len);
            }
            log.debug("文件读取完成");
            return result.toByteArray();
        } catch (OSSException | ClientException | IOException oe) {
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"获取阿里云图片地址失败");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }


    /**
     * 获取文件地址
     * @param fileId 文件id
     * @return 文件访问地址
     */
    public String getFileUrl(Long fileId) throws BaseException {
        ComFile exist = fileService.getById(fileId);
        if(exist != null) {
            // 判断存储类型，对内容做不同的处理
            FileStoreType storeType = FileStoreType.valueOf(exist.getStoreType());
            switch (storeType) {
                case aliyun:
                    return getFileUrlFromAliyunOss(exist);
                case local:
                    return "";
                case table:
                    return fileConfig.downloadPrefix + exist.getId();
                default:
                    throw new BaseException(ResultCode.DATA_NOT_FOUND, "文件存储方式配置有误");
            }
        }else {
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"没有对应文件");
        }
    }

    /**
     * 从阿里云oss下载文件
     * @return
     */
    private String getFileUrlFromAliyunOss(ComFile file) throws BaseException {
        if(StringUtils.isBlank(file.getLocation())) {
            log.error("对应文件{}访问地址数据丢失", file.getId());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"对应文件访问地址数据丢失");
        }
        // 创建OSSClient实例。
        OSS ossClient = getAliyunOss();

        try {
            // 设置URL过期时间为1小时。
            Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
        // 生成以GET方法访问的签名URL，访客可以直接通过浏览器访问相关内容。
            URL url = ossClient.generatePresignedUrl(fileConfig.aliyunBucket, file.getLocation(), expiration);
            return url.toString();
        } catch (OSSException | ClientException oe) {
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"获取阿里云图片地址失败");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

    /**
     * 删除文件
     * @param fileId 文件id
     * @throws BaseException 删除错误
     */
    @Transactional(rollbackFor = Exception.class)
    public void delete(Integer fileId) throws BaseException {
        ComFile file = fileService.getById(fileId);
        if(file==null) {
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"没有对应文件");
        }
        if(StringUtils.isNotBlank(file.getLocation())) {
            // 文件内容不为空，且内容被引用次数为1，则删除
            int count = fileService.countByFileContent(file.getStoreType(), file.getLocation());
            if(count<=1) {
                // 判断存储类型，对内容做不同的处理
                FileStoreType storeType = FileStoreType.valueOf(file.getStoreType());
                switch (storeType) {
                    case aliyun:
                        deleteContentFromAliyunOss(file);
                        break;
                    case table:
                        deleteContentFromTable(file);
                        break;
                    case local:
                    default:
                        throw new BaseException(ResultCode.DATA_NOT_FOUND, "文件存储方式配置有误");
                }
            }
        }
        // 删除文件记录
        if(fileService.removeById(fileId)){
            log.error("删除文件[{}]失败", fileId);
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"删除文件失败"+fileId);
        }
    }

    private void deleteContentFromAliyunOss(ComFile file) throws BaseException {
        // 创建OSSClient实例。
        OSS ossClient = getAliyunOss();

        try {
            // 删除文件。如需删除文件夹，请将ObjectName设置为对应的文件夹名称。如果文件夹非空，则需要将文件夹下的所有object删除后才能删除该文件夹。
            ossClient.deleteObject(fileConfig.aliyunBucket, file.getLocation());

        } catch (OSSException | ClientException oe) {
            log.error("从阿里云删除文件{}内容失败{}", file.getId(), file.getLocation());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"删除文件内容失败");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

    /**
     * 从数据库里删除文件内容, 判断被引用计数是否为0
     * @return 删除是否成功
     */
    private void deleteContentFromTable(ComFile file) throws BaseException {
        if(StringUtils.isBlank(file.getLocation())) {
            return;
        }
        // 计算被引用计数

        Integer contentId = Integer.valueOf(file.getLocation());
        boolean b = fileContentService.removeById(contentId);
        if(!b) {
            log.error("文件{}删除内容{}失败", file.getId(), file.getLocation());
            throw new BaseException(ResultCode.DATA_NOT_FOUND,"删除内容失败");
        }
    }


    /**
     * 其他适用图片的业务实体
     * 将文件从oldId替换成newId
     * 因此要删掉oldId对应的文件
     * @param referenceKey 使用图片业务实体的唯一key，通常是实体名加id
     * @param oldFileId 原有文件id
     * @param newFileId 更新文件id
     */
    public void changeFile(String referenceKey,Long oldFileId,Long newFileId) throws BaseException {
        //判断id是否一致
        if(oldFileId != null && oldFileId.equals(newFileId)){
            return;
        }
        //之前是否oldfileid 有可能为空
        //存在先删除 空直接新建
        if(oldFileId != null){
            fileReferenceService.delByKeyAndFileId(referenceKey, oldFileId);
        }
        //查看新的引用关系是否存在 不存在则新建
        ComFileReference existNew = fileReferenceService.getByByKeyAndFileId(referenceKey, newFileId);
        if(existNew != null){
            return;
        }
        fileReferenceService.saveOrUpdateAndReturn(ComFileReference.created(newFileId,referenceKey));
    }


}
